import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split

# 1. 收集数据
# 2. 将收集到的数据可视化，显示出来
# 3. 做特征工程，使数据更容易被机器处理
# 4. 拆分数据集为训练集和测试集
# 5. 做特征缩放，把数据值压缩到比较小的区间


def shop_demo():
    df_ads = pd.read_csv('advertising.csv')
    sns.heatmap(df_ads.corr(), cmap="BuPu", annot = True) # 显示特征相关性，1到-1，1代表正相关，-1代表负相关
    plt.show()

    # 显示销售额和各种广告投放金额的散点图
    sns.pairplot(df_ads,
                 x_vars=['wechat', 'weibo', 'others'],
                 y_vars='sales',
                 height=4,
                 aspect=1,
                 kind='scatter')
    plt.show()

    X = np.array(df_ads.wechat) # 构建特征集，只含有微信公众号广告投放金额一个特征
    y = np.array(df_ads.sales) # 构建标签集，销售额

    X = X.reshape((len(X), 1)) # 通过reshape方法把向量转换为矩阵，len函数返回样本个数
    y = y.reshape((len(y), 1)) # 通过reshape方法把向量转换为矩阵，len函数返回样本个数

    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) # 拆分数据集为训练集，测试集（占比20%）

    # 数据归一化，数据压缩：对数据进行归一化，归一化是按比例的线性缩放；数据归一化之后，数据分布不变，但是都落入一个小的特定区间
    X_train, X_test = scaler(X_train, X_test) # 对特征归一化
    y_train, y_test = scaler(y_train, y_test) # 对标签归一化

    plt.plot(X_train, y_train, 'r', label = 'Training data')
    plt.xlabel('wechat')
    plt.ylabel('scales')
    plt.legend()
    plt.show()

    # 确定线性回归模型 y = wx + b
    # 损失（误差）函数 L(w, b): 针对每一组不同的参数，机器都会针对样本数据计算一次平均损失，损失函数L(w, b)就是用来计算平均损失的
    # 计算当前假设函数所造成的损失的过程，就是模型内部参数的评估的过程

    # 用于回归的损失函数:
    # 1.均方误差函数（平方损失或L2损失函数）
    # 2.平均绝对误差函数（L1）损失函数
    # 3.平均偏差误差函数

    # 用于分类的损失函数
    # 1.交叉熵损失函数
    # 2.多分类SVM损失

    # 首先确定参数的初始值
    iterations = 100 # 迭代100次
    alpha = 1 #初始学习速率
    weight = -5 # 权重
    bias = 3 # 偏置

    # 根据初始参数值，进行梯度下降，开始拟合函数
    loss_history, weight_history, bias_history = gradient_descent(X_train, y_train, weight, bias, alpha, iterations)
    plt.plot(loss_history, 'g--', label='Loss Curve') # 显示损失曲线
    plt.xlabel('Iterations') # x轴标签
    plt.ylabel('Loss') # y轴标签
    plt.legend() # 显示图例
    plt.show()# 显示损失曲线

    # 绘制当前的函数模型
    plt.plot(X_train, y_train, 'r.',label='Training data') # 显示训练数据
    line_X = np.linspace(X_train.min(), X_train.max(), 500) # X值域
    line_y = [weight_history[-1]*xx + bias_history[-1] for xx in line_X] #假设函数
    plt.plot(line_X,line_y,'b--',label='Current hypothesis') # 显示当前假设函数
    plt.xlabel('wechat') # x轴标签
    plt.ylabel('sales') # y轴标签
    plt.legend()
    plt.show()

    print("\n当前权重: ", weight_history[-1])
    print("\n当前偏置: ", bias_history[-1])



# 梯度下降：机器学习的精髓，每一次拟合猜测的结果，都比上一次的猜测结果更接近真相
# 学习速率：学习速率乘以损失函数求导之后的微分值，就是一次梯度变化的步长；学习速率控制着当前梯度下降的节奏，或快或慢
#   在机器学习刚开始的时候，学习速率可以设置得大一些，快速几步达到靠近最佳权重的位置，当逐渐地接近最佳权重时，减小学习速率
def gradient_descent(X, y, w, b, lr, iter): # 定义一个实现梯度下降的函数
    l_history = np.zeros(iter) # 初始化记录梯度下降过程中损失的数组
    w_history = np.zeros(iter) # 初始化记录梯度下降过程中权重的数组
    b_history = np.zeros(iter) # 初始化记录梯度下降过程中偏置的数组
    for i in range(iter): # 进行梯度下降的迭代，就是下多少级台阶
        y_hat = w*X + b # 向量化运算实现的假设函数
        loss = y_hat - y # 这是中间过程，求得的是假设函数预测的y^和真正的y值之间的差值
        derivative_w = X.T.dot(loss)/len(X) # 对权重求导，len(X)是样本总数
        derivative_b = sum(loss)*1/len(X) # 对偏置求导
        w = w - lr*derivative_w # 结合学习速率，更新权重
        b = b - lr*derivative_b # 结合学习速率，更新偏置
        l_history[i] = loss_function(X,y,w,b) # 梯度下降过程中损失的历史记录
        w_history[i] = w # 梯度下降过程中权重的历史记录
        b_history[i] = b # 梯度下降过程中偏置的历史记录
    return l_history, w_history, b_history # 返回数据


# 手工定义一个均方误差函数
def loss_function(X, y, weight, bias):
    y_hat = weight*X + bias # 这是假设函数，其中已经应用了Python的广播功能
    loss = y_hat - y # 求出每一个y^和训练集中真实的y之间的差异
    cost = np.sum(loss**2)/2*len(X) # 这是均方差误差函数的代码实现 **代表平方
    return cost # 返回当前模型的均方误差值
    ## 为什么要取平方值：为了让L(w, b)函数形成相对于w和b而言的凸函数，实现梯度下降


# 数据归一化函数
def scaler(train, test):
    min = train.min(axis=0) # 训练集最小值
    max = train.max(axis=0) # 训练集最大值
    gap = max - min # 最大值和最小值的差
    train -= min # 所有数据减去最小值
    train /=gap # 所有数据除以最大值和最小值的差
    test -=min # 把训练集最小值应用于测试集
    test /= gap # 把训练集最大值和最小值的差应用于测试集
    return train, test # 返回压缩后的数据

if __name__ == '__main__':
    shop_demo();